@easytwin/devkit 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -7,13 +7,23 @@ 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 path5 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
- var DEFAULT_BASE_URL = "https://api.easytwin.example.com";
16
+ var DEFAULT_BASE_URL = "http://saas-twin.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";
21
+ var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
22
+ var MOCK_APP_ID = "test";
23
+ var MOCK_APP_SECRET = "test";
24
+ function isMockCredentials(appId, appSecret) {
25
+ return appId === MOCK_APP_ID && appSecret === MOCK_APP_SECRET;
26
+ }
17
27
  var ConfigError = class extends Error {
18
28
  constructor(message) {
19
29
  super(message);
@@ -46,10 +56,65 @@ function validateConfigShape(value) {
46
56
  if (v.baseUrl !== void 0 && (typeof v.baseUrl !== "string" || v.baseUrl.length === 0)) {
47
57
  throw new ConfigError("baseUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
48
58
  }
59
+ if (v.ossUrl !== void 0 && (typeof v.ossUrl !== "string" || v.ossUrl.length === 0)) {
60
+ throw new ConfigError("ossUrl \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
61
+ }
62
+ if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
63
+ throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
64
+ }
65
+ const opAccountId = optionalGatewayId(v.opAccountId);
66
+ const opUserId = optionalGatewayId(v.opUserId);
67
+ const spaceId = optionalGatewayId(v.spaceId);
49
68
  const config = { appId: v.appId, appSecret: v.appSecret };
69
+ if (v.env === "prod" || v.env === "test") config.env = v.env;
50
70
  if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
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;
51
77
  return config;
52
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
+ }
53
118
  async function readConfigFile(cwd) {
54
119
  const file = configFilePath(cwd);
55
120
  let raw;
@@ -63,19 +128,50 @@ async function readConfigFile(cwd) {
63
128
  function resolveConfig(file, env = process.env) {
64
129
  const appId = env.EASYTWIN_APP_ID ?? file.appId;
65
130
  const appSecret = env.EASYTWIN_APP_SECRET ?? file.appSecret;
66
- const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? DEFAULT_BASE_URL;
131
+ const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file.env ?? "prod";
132
+ const baseUrl = env.EASYTWIN_BASE_URL ?? file.baseUrl ?? (easyEnv === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL);
133
+ const ossUrl = env.EASYTWIN_OSS_URL ?? file.ossUrl ?? DEFAULT_OSS_URL;
67
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");
68
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");
69
136
  const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
70
- return { appId, appSecret, baseUrl, 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;
71
146
  }
72
147
  async function loadConfig(cwd, env = process.env) {
73
- 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;
74
161
  }
75
162
  async function writeConfigFile(cwd, config) {
76
163
  const file = configFilePath(cwd);
77
164
  const body = { appId: config.appId, appSecret: config.appSecret };
165
+ if (config.env) body.env = config.env;
78
166
  if (config.baseUrl) body.baseUrl = config.baseUrl;
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;
79
175
  await fs.mkdir(cwd, { recursive: true });
80
176
  await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
81
177
  return file;
@@ -103,18 +199,31 @@ async function initConfig(input, cwd) {
103
199
  const gitignore = await appendGitignore(cwd);
104
200
  return { configFile, gitignore };
105
201
  }
202
+ async function writeConfigScenes(cwd, scenes) {
203
+ const file = await readConfigFile(cwd);
204
+ await writeConfigFile(cwd, { ...file, scenes });
205
+ }
106
206
 
107
207
  // src/client.ts
108
208
  import http from "http";
109
209
  import https from "https";
110
210
  import { URL as URL2 } from "url";
111
- var AUTH_HEADER = "Authorization";
112
- var BEARER_PREFIX = "Bearer";
113
- 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
+ }
114
218
  var ENDPOINTS = {
115
- scenes: "/api/scenes",
116
- scene: (id) => `/api/scenes/${encodeURIComponent(id)}`,
117
- 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`
118
227
  };
119
228
  var EasyTwinApiError = class extends Error {
120
229
  status;
@@ -142,25 +251,50 @@ function parseResponseBody(buffer) {
142
251
  return text;
143
252
  }
144
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
+ }
145
265
  var EasyTwinClient = class {
146
266
  baseUrl;
267
+ /** twin runtime / 场景快照资产根(baseOSSUrl)。 */
268
+ ossUrl;
269
+ /** 接入凭证 App ID;同时作为场景/代码 API 的 applicationId 路径参数。 */
147
270
  appId;
271
+ /** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
272
+ mock;
148
273
  appSecret;
274
+ opAccountId;
275
+ opUserId;
276
+ spaceId;
149
277
  constructor(config) {
150
278
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
279
+ this.ossUrl = config.ossUrl.replace(/\/+$/, "");
280
+ this.mock = config.mock;
151
281
  this.appId = config.appId;
152
282
  this.appSecret = config.appSecret;
283
+ this.opAccountId = config.opAccountId;
284
+ this.opUserId = config.opUserId;
285
+ this.spaceId = config.spaceId;
153
286
  }
154
287
  /** 全仓唯一认证头注入点。 */
155
288
  authHeaders() {
156
- return {
157
- [AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
158
- [APP_ID_HEADER]: this.appId
159
- };
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;
160
294
  }
161
295
  /** JSON 请求(原生 fetch)。 */
162
- async request(path6, options = {}) {
163
- const url = `${this.baseUrl}${path6}`;
296
+ async request(path9, options = {}) {
297
+ const url = `${this.baseUrl}${path9}`;
164
298
  const headers = { ...this.authHeaders(), ...options.headers };
165
299
  if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
166
300
  headers["Content-Type"] = "application/json";
@@ -182,8 +316,8 @@ var EasyTwinClient = class {
182
316
  * multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
183
317
  * 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
184
318
  */
185
- async upload(path6, options) {
186
- const url = new URL2(`${this.baseUrl}${path6}`);
319
+ async upload(path9, options) {
320
+ const url = new URL2(`${this.baseUrl}${path9}`);
187
321
  const mod = url.protocol === "https:" ? https : http;
188
322
  const headers = {
189
323
  ...this.authHeaders(),
@@ -216,8 +350,8 @@ var EasyTwinClient = class {
216
350
  return this.handleStatus(raw.status, raw.body);
217
351
  }
218
352
  handleStatus(status, body) {
219
- if (status >= 200 && status < 300) return parseResponseBody(body);
220
353
  const parsed = parseResponseBody(body);
354
+ if (status >= 200 && status < 300) return unwrapBody(parsed, status);
221
355
  throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
222
356
  }
223
357
  };
@@ -225,25 +359,146 @@ var EasyTwinClient = class {
225
359
  // src/scene.ts
226
360
  import { promises as fs2 } from "fs";
227
361
  import path2 from "path";
228
- function normalizeSceneList(data) {
229
- const list = Array.isArray(data) ? data : data?.list;
230
- if (!Array.isArray(list)) throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
362
+ import { fileURLToPath } from "url";
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);
231
385
  return list.map((item) => {
232
- const it = item ?? {};
233
- 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
+ };
403
+ });
404
+ }
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;
234
421
  });
235
422
  }
236
- function normalizeSceneDetail(data) {
237
- const it = data ?? {};
238
- return { id: String(it.id ?? ""), name: String(it.name ?? ""), payload: data };
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
+ }
446
+ }
447
+ var EXAMPLE_SCENE_FILE = "scene.example.json";
448
+ var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
449
+ function resolveExampleScenePath() {
450
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
451
+ throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D ${EXAMPLE_SCENE_FILE}:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 exampleFile`);
452
+ }
453
+ return path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..", EXAMPLE_SCENE_FILE);
454
+ }
455
+ async function loadExampleScene(exampleFile) {
456
+ const file = exampleFile ?? resolveExampleScenePath();
457
+ let raw;
458
+ try {
459
+ raw = await fs2.readFile(file, "utf8");
460
+ } catch {
461
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730\u793A\u4F8B\u573A\u666F ${file}(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F\u9700\u8981 devkit \u5305\u5185\u7684 ${EXAMPLE_SCENE_FILE})`);
462
+ }
463
+ try {
464
+ return JSON.parse(raw);
465
+ } catch {
466
+ throw new Error(`\u672C\u5730\u793A\u4F8B\u573A\u666F\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
467
+ }
468
+ }
469
+ function deriveExampleSceneId(payload) {
470
+ const root = payload ?? {};
471
+ const id = root?.objs?.map((o) => o.sceneId).find((s) => typeof s === "string" && s.length > 0);
472
+ return id ?? "local";
239
473
  }
240
- async function listScenes(client) {
241
- const data = await client.request(ENDPOINTS.scenes, { method: "GET" });
242
- return normalizeSceneList(data);
474
+ async function exampleSceneSummary(exampleFile) {
475
+ return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
243
476
  }
244
- async function pullScene(client, id) {
245
- const data = await client.request(ENDPOINTS.scene(id), { method: "GET" });
246
- return normalizeSceneDetail(data);
477
+ async function listScenes(client, options = {}) {
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;
481
+ }
482
+ async function pullScene(client, id, options = {}) {
483
+ if (client.mock) {
484
+ const payload2 = await loadExampleScene(options.exampleFile);
485
+ const sceneId = deriveExampleSceneId(payload2);
486
+ if (sceneId !== id) {
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)`);
488
+ }
489
+ return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
490
+ }
491
+ const data = await client.request(ENDPOINTS.linkedScenes(client.appId), { method: "GET" });
492
+ const scenes = normalizeLinkedScenes(data);
493
+ const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
494
+ if (!hit) {
495
+ const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
496
+ throw new Error(
497
+ available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
498
+ );
499
+ }
500
+ const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
501
+ return { id: hit.sceneKey, name: hit.name, payload };
247
502
  }
248
503
  async function saveSceneJson(scene, out) {
249
504
  await fs2.mkdir(path2.dirname(out), { recursive: true });
@@ -276,59 +531,389 @@ async function collectFiles(dir, ignore = () => false) {
276
531
  files.sort((a, b) => a.relPath.localeCompare(b.relPath));
277
532
  return files;
278
533
  }
279
- function buildMultipartBody(boundary, fields, file) {
280
- const parts = [];
281
- for (const [key, value] of Object.entries(fields)) {
282
- parts.push(
283
- Buffer.from(`--${boundary}\r
284
- Content-Disposition: form-data; name="${key}"\r
285
- \r
286
- ${value}\r
287
- `, "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(", ")}`
288
589
  );
289
590
  }
290
- parts.push(
291
- Buffer.from(
292
- `--${boundary}\r
293
- Content-Disposition: form-data; name="file"; filename="${file.name}"\r
294
- Content-Type: application/octet-stream\r
295
- \r
296
- `,
297
- "utf8"
298
- )
299
- );
300
- parts.push(file.content);
301
- parts.push(Buffer.from(`\r
302
- --${boundary}--\r
303
- `, "utf8"));
304
- 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("; ")}`);
601
+ }
602
+ async function mockUpload(dir, options) {
603
+ const ignore = options.ignore ?? (() => false);
604
+ const files = await collectFiles(dir, ignore);
605
+ const total = files.reduce((sum, f) => sum + f.size, 0);
606
+ options.onProgress?.({ phase: "collect", current: total, total });
607
+ options.onProgress?.({ phase: "upload", current: total, total });
608
+ return { fileCount: files.length, byteCount: total, mock: true };
305
609
  }
306
610
  async function uploadDirectory(client, dir, options = {}) {
611
+ if (client.mock) return mockUpload(dir, options);
307
612
  const ignore = options.ignore ?? (() => false);
308
613
  const files = await collectFiles(dir, ignore);
309
614
  const total = files.reduce((sum, f) => sum + f.size, 0);
310
615
  options.onProgress?.({ phase: "collect", current: total, total });
311
- 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 = [];
312
632
  for (const file of files) {
313
- const content = await fs3.readFile(file.absPath);
314
- const boundary = `----easytwin${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
315
- const body = buildMultipartBody(
316
- boundary,
317
- // TODO(swagger): multipart 字段名/结构待定,当前为占位约定。
318
- { path: file.relPath },
319
- { name: path3.basename(file.relPath), content }
320
- );
321
- 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) => {
322
641
  sent += file.size;
323
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);
650
+ }
651
+ for (const c of creates) {
652
+ await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
653
+ method: "POST",
654
+ body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
655
+ });
656
+ bump(c.file);
324
657
  }
325
- return { fileCount: files.length, byteCount: sent };
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 };
326
661
  }
327
662
 
328
- // src/skills.ts
663
+ // src/workspace.ts
329
664
  import { promises as fs4 } from "fs";
665
+ import path5 from "path";
666
+
667
+ // src/assetsPath.ts
330
668
  import path4 from "path";
331
- import { fileURLToPath } from "url";
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";
916
+ import { fileURLToPath as fileURLToPath2 } from "url";
332
917
  var SKILL_NAMES = [
333
918
  "easytwin-render",
334
919
  "easytwin-core",
@@ -340,8 +925,81 @@ var SKILL_NAMES = [
340
925
  var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
341
926
  var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
342
927
  var META_FILE_NAME = ".easytwin-meta.json";
928
+ var MINIMAL_TSCONFIG = `${JSON.stringify(
929
+ {
930
+ compilerOptions: {
931
+ target: "ES2022",
932
+ module: "ESNext",
933
+ moduleResolution: "bundler",
934
+ strict: true,
935
+ skipLibCheck: true,
936
+ noEmit: true,
937
+ paths: {
938
+ "@easytwin/runtime": [".easytwin/types"],
939
+ "@easytwin/apps": [".easytwin/types/apps"]
940
+ }
941
+ },
942
+ include: ["src/**/*.ts"]
943
+ },
944
+ null,
945
+ 2
946
+ )}
947
+ `;
948
+ var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
949
+ "skipLibCheck": true,
950
+ "paths": {
951
+ "@easytwin/runtime": [".easytwin/types"],
952
+ "@easytwin/apps": [".easytwin/types/apps"]
953
+ }`;
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
+ };
962
+ engine: RuntimeEngine;
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;
993
+ }
994
+
995
+ export declare function defineApp<T>(app: T): T;
996
+ `;
997
+ function buildRuntimeTypesContent(sourceDts) {
998
+ return `${sourceDts.replace(/\s+$/, "")}
999
+ `;
1000
+ }
343
1001
  function normalizeTargets(target = "all") {
344
- if (target === "all") return ["cursor", "claude", "codex"];
1002
+ if (target === "all") return ["cursor", "claude", "codex", "qoder"];
345
1003
  if (Array.isArray(target)) return [...new Set(target)];
346
1004
  return [target];
347
1005
  }
@@ -349,58 +1007,69 @@ function resolveSkillsSourceDir() {
349
1007
  if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
350
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");
351
1009
  }
352
- const here = path4.dirname(fileURLToPath(import.meta.url));
353
- return path4.resolve(here, "..", "skills");
1010
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1011
+ return path6.resolve(here, "..", "skills");
1012
+ }
1013
+ function resolveRuntimeTypesSourceFile() {
1014
+ if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
1015
+ throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
1016
+ }
1017
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
1018
+ const fromDist = path6.join(here, "runtime-types", "index.d.ts");
1019
+ const fromSrc = path6.resolve(here, "lib", "index.d.ts");
1020
+ if (existsSync(fromDist)) return fromDist;
1021
+ if (existsSync(fromSrc)) return fromSrc;
1022
+ throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
354
1023
  }
355
1024
  async function readJson(file) {
356
- return JSON.parse(await fs4.readFile(file, "utf8"));
1025
+ return JSON.parse(await fs5.readFile(file, "utf8"));
357
1026
  }
358
1027
  async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
359
- const marker = path4.join(skillsDir, ".easytwin-source.json");
1028
+ const marker = path6.join(skillsDir, ".easytwin-source.json");
360
1029
  try {
361
1030
  const meta = await readJson(marker);
362
1031
  if (typeof meta.version === "string") return meta.version;
363
1032
  } catch {
364
1033
  }
365
1034
  try {
366
- const pkg = await readJson(path4.resolve(skillsDir, "..", "package.json"));
1035
+ const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
367
1036
  return pkg.version;
368
1037
  } catch {
369
1038
  return "0.0.0";
370
1039
  }
371
1040
  }
372
1041
  async function copyDir(src, dest) {
373
- await fs4.mkdir(dest, { recursive: true });
374
- const entries = await fs4.readdir(src, { withFileTypes: true });
1042
+ await fs5.mkdir(dest, { recursive: true });
1043
+ const entries = await fs5.readdir(src, { withFileTypes: true });
375
1044
  for (const entry of entries) {
376
- const s = path4.join(src, entry.name);
377
- const d = path4.join(dest, entry.name);
1045
+ const s = path6.join(src, entry.name);
1046
+ const d = path6.join(dest, entry.name);
378
1047
  if (entry.isDirectory()) await copyDir(s, d);
379
- else if (entry.isFile()) await fs4.copyFile(s, d);
1048
+ else if (entry.isFile()) await fs5.copyFile(s, d);
380
1049
  }
381
1050
  }
382
1051
  async function dirMatches(src, dest) {
383
1052
  let sourceEntries;
384
1053
  try {
385
- sourceEntries = await fs4.readdir(src);
1054
+ sourceEntries = await fs5.readdir(src);
386
1055
  } catch {
387
1056
  return false;
388
1057
  }
389
1058
  for (const name of sourceEntries) {
390
- const s = path4.join(src, name);
391
- const d = path4.join(dest, name);
392
- 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);
393
1062
  if (sStat.isDirectory()) {
394
1063
  if (!await dirMatches(s, d)) return false;
395
1064
  } else {
396
1065
  let dStat;
397
1066
  try {
398
- dStat = await fs4.stat(d);
1067
+ dStat = await fs5.stat(d);
399
1068
  } catch {
400
1069
  return false;
401
1070
  }
402
1071
  if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
403
- 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;
404
1073
  }
405
1074
  }
406
1075
  return true;
@@ -411,18 +1080,18 @@ function actionFor(exists, matches) {
411
1080
  }
412
1081
  async function writeMeta(destDir, version) {
413
1082
  const meta = { name: "@easytwin/devkit", version };
414
- 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");
415
1084
  }
416
1085
  async function syncToDir(target, sourceRoot, targetRoot, version) {
417
1086
  const entries = [];
418
1087
  for (const name of SKILL_NAMES) {
419
- const src = path4.join(sourceRoot, name);
420
- const dest = path4.join(targetRoot, name);
421
- 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);
422
1091
  const matches = await dirMatches(src, dest);
423
1092
  const action = actionFor(exists, matches);
424
1093
  if (action !== "unchanged") {
425
- await fs4.rm(dest, { recursive: true, force: true });
1094
+ await fs5.rm(dest, { recursive: true, force: true });
426
1095
  await copyDir(src, dest);
427
1096
  }
428
1097
  entries.push({ name, action });
@@ -441,19 +1110,19 @@ function buildCodexSegment(version) {
441
1110
  "- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
442
1111
  "- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
443
1112
  "- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
444
- "- `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",
445
1114
  "",
446
1115
  "\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
447
1116
  CODEX_MARKER_END
448
1117
  ].join("\n");
449
1118
  }
450
1119
  async function syncToCodex(sourceRoot, cwd, version) {
451
- const agentsFile = path4.join(cwd, "AGENTS.md");
1120
+ const agentsFile = path6.join(cwd, "AGENTS.md");
452
1121
  const segment = buildCodexSegment(version);
453
1122
  let content = "";
454
1123
  let exists = true;
455
1124
  try {
456
- content = await fs4.readFile(agentsFile, "utf8");
1125
+ content = await fs5.readFile(agentsFile, "utf8");
457
1126
  } catch {
458
1127
  exists = false;
459
1128
  }
@@ -471,22 +1140,201 @@ async function syncToCodex(sourceRoot, cwd, version) {
471
1140
  next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
472
1141
  }
473
1142
  if (action !== "unchanged") {
474
- await fs4.mkdir(cwd, { recursive: true });
475
- await fs4.writeFile(agentsFile, next, "utf8");
1143
+ await fs5.mkdir(cwd, { recursive: true });
1144
+ await fs5.writeFile(agentsFile, next, "utf8");
476
1145
  }
477
1146
  return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
478
1147
  }
1148
+ async function syncRuntimeTypes(cwd, typesSourceFile) {
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;
1157
+ try {
1158
+ runtimeCurrent = await fs5.readFile(destFile, "utf8");
1159
+ } catch {
1160
+ runtimeExists = false;
1161
+ }
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");
1174
+ }
1175
+ const tsconfigPath = path6.join(cwd, "tsconfig.json");
1176
+ let tsconfig;
1177
+ let pathsHint;
1178
+ try {
1179
+ const existing = await fs5.readFile(tsconfigPath, "utf8");
1180
+ if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
1181
+ else {
1182
+ tsconfig = "manual-paths";
1183
+ pathsHint = TSCONFIG_PATHS_HINT;
1184
+ }
1185
+ } catch {
1186
+ await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
1187
+ tsconfig = "created";
1188
+ }
1189
+ return { action, tsconfig, pathsHint };
1190
+ }
479
1191
  async function syncSkills(options) {
480
1192
  const targets = normalizeTargets(options.targets ?? "all");
481
1193
  const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
482
1194
  const version = options.version ?? await readDevkitVersion(sourceRoot);
483
1195
  const summaries = [];
484
1196
  for (const target of targets) {
485
- if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
486
- else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "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));
487
1200
  else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
488
1201
  }
489
- return summaries;
1202
+ const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
1203
+ return { summaries, types };
1204
+ }
1205
+
1206
+ // src/bundle.ts
1207
+ import * as esbuild from "esbuild-wasm";
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
1230
+ var USER_ENTRY = "src/main.ts";
1231
+ var DEFAULT_BUNDLE_OUT = "dist/main.js";
1232
+ var RUNTIME_MODULE = "@easytwin/runtime";
1233
+ var APPS_MODULE = "@easytwin/apps";
1234
+ var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
1235
+ var BundleError = class extends Error {
1236
+ constructor(message) {
1237
+ super(message);
1238
+ this.name = "BundleError";
1239
+ }
1240
+ };
1241
+ function isRelativeOrAbsolute(spec) {
1242
+ return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
1243
+ }
1244
+ function whitelistPlugin() {
1245
+ return {
1246
+ name: "easytwin-whitelist",
1247
+ setup(build2) {
1248
+ build2.onResolve({ filter: /.*/ }, (args) => {
1249
+ if (args.kind === "entry-point") return void 0;
1250
+ if (isRelativeOrAbsolute(args.path)) return void 0;
1251
+ if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
1252
+ return {
1253
+ errors: [
1254
+ {
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`
1256
+ }
1257
+ ]
1258
+ };
1259
+ });
1260
+ }
1261
+ };
1262
+ }
1263
+ function formatEsbuildMessages(messages) {
1264
+ return messages.map((m) => {
1265
+ const loc = m.location ? `${m.location.file}:${m.location.line}:${m.location.column}: ` : "";
1266
+ return `${loc}${m.text}`;
1267
+ }).join("\n");
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
+ }
1284
+ async function bundleUserCode(options) {
1285
+ const cwd = path7.resolve(options.cwd);
1286
+ const entry = path7.join(cwd, USER_ENTRY);
1287
+ try {
1288
+ await fs6.access(entry);
1289
+ } catch {
1290
+ throw new BundleError(
1291
+ `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
1292
+ );
1293
+ }
1294
+ let result;
1295
+ try {
1296
+ result = await esbuild.build({
1297
+ absWorkingDir: cwd,
1298
+ entryPoints: [entry],
1299
+ bundle: true,
1300
+ write: false,
1301
+ format: "esm",
1302
+ platform: "browser",
1303
+ target: "es2022",
1304
+ sourcemap: "inline",
1305
+ logLevel: "silent",
1306
+ plugins: [whitelistPlugin()]
1307
+ });
1308
+ } catch (err) {
1309
+ const errors = err.errors;
1310
+ if (Array.isArray(errors) && errors.length > 0) {
1311
+ throw new BundleError(formatEsbuildMessages(errors));
1312
+ }
1313
+ throw err instanceof Error ? new BundleError(err.message) : err;
1314
+ }
1315
+ if (result.errors.length > 0) {
1316
+ throw new BundleError(formatEsbuildMessages(result.errors));
1317
+ }
1318
+ const file = result.outputFiles?.[0];
1319
+ if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
1320
+ const code = file.text;
1321
+ const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
1322
+ const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
1323
+ if (portable) warnings.push(portable);
1324
+ if (options.outFile) {
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");
1328
+ }
1329
+ return { code, warnings };
1330
+ }
1331
+ async function typesMissingHint(cwd) {
1332
+ try {
1333
+ await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
1334
+ return void 0;
1335
+ } catch {
1336
+ return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
1337
+ }
490
1338
  }
491
1339
 
492
1340
  // src/bin.ts
@@ -498,6 +1346,7 @@ async function readVersion() {
498
1346
  return "0.0.0";
499
1347
  }
500
1348
  }
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";
501
1350
  async function prompt(question) {
502
1351
  const rl = createInterface({ input: stdin, output: stdout });
503
1352
  try {
@@ -509,8 +1358,17 @@ async function prompt(question) {
509
1358
  async function runInit(cwd, flags) {
510
1359
  const appId = flags.appId ?? await prompt("App ID: ");
511
1360
  const appSecret = flags.appSecret ?? await prompt("App Secret: ");
512
- const baseUrlInput = flags.baseUrl ?? await prompt(`Base URL(\u7F3A\u7701 ${DEFAULT_BASE_URL}): `);
1361
+ let env = "prod";
1362
+ if (flags.env !== void 0) {
1363
+ env = flags.env.toLowerCase() === "test" ? "test" : "prod";
1364
+ } else if (flags.baseUrl === void 0) {
1365
+ const envRaw = (await prompt("\u73AF\u5883(prod=\u6B63\u5F0F / test=\u6D4B\u8BD5,\u7F3A\u7701 prod): ")).trim().toLowerCase();
1366
+ env = envRaw === "test" ? "test" : "prod";
1367
+ }
1368
+ const defaultUrl = env === "test" ? TEST_BASE_URL : DEFAULT_BASE_URL;
1369
+ const baseUrlInput = flags.baseUrl ?? await prompt(`Base URL(\u7F3A\u7701 ${defaultUrl}): `);
513
1370
  const input = { appId, appSecret };
1371
+ if (env === "test") input.env = "test";
514
1372
  if (baseUrlInput.trim().length > 0) input.baseUrl = baseUrlInput.trim();
515
1373
  const result = await initConfig(input, cwd);
516
1374
  console.log(`\u5DF2\u751F\u6210 ${result.configFile}`);
@@ -521,28 +1379,40 @@ async function runInit(cwd, flags) {
521
1379
  function buildProgram(cwd, version = "0.0.0") {
522
1380
  const program = new Command();
523
1381
  program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
524
- 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("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
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));
525
1383
  const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
526
- 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 () => {
527
1385
  const config = await loadConfig(cwd);
1386
+ if (config.mock) console.log(MOCK_MODE_HINT);
528
1387
  const client = new EasyTwinClient(config);
529
- const scenes = await listScenes(client);
1388
+ const scenes = await listScenes(client, { cwd });
530
1389
  if (scenes.length === 0) {
531
1390
  console.log("(\u65E0\u573A\u666F)");
532
- return;
1391
+ } else {
1392
+ for (const s of scenes) console.log(`${s.id} ${s.name}`);
533
1393
  }
534
- 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}`);
535
1395
  });
536
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) => {
537
1397
  const config = await loadConfig(cwd);
1398
+ if (config.mock) console.log(MOCK_MODE_HINT);
538
1399
  const client = new EasyTwinClient(config);
539
1400
  const scene2 = await pullScene(client, id);
540
- const out = opts.out ?? path5.join(cwd, `${id}.scene.json`);
1401
+ const out = opts.out ?? path8.join(cwd, `${id}.scene.json`);
541
1402
  const file = await saveSceneJson(scene2, out);
542
1403
  console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
543
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
+ });
544
1413
  program.command("upload <dir>").description("\u5168\u91CF\u8986\u76D6\u4E0A\u4F20\u76EE\u5F55(\u4E0D\u53EF\u9006)").action(async (dir) => {
545
1414
  const config = await loadConfig(cwd);
1415
+ if (config.mock) console.log(MOCK_MODE_HINT);
546
1416
  const client = new EasyTwinClient(config);
547
1417
  await uploadDirectory(client, dir, {
548
1418
  onProgress: (p) => {
@@ -553,9 +1423,17 @@ function buildProgram(cwd, version = "0.0.0") {
553
1423
  });
554
1424
  process.stderr.write("\n");
555
1425
  });
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) => {
1427
+ const hint = await typesMissingHint(cwd);
1428
+ if (hint) console.warn(hint);
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);
1432
+ console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
1433
+ });
556
1434
  const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
557
- skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|all", "all").action(async (opts) => {
558
- const summaries = await syncSkills({ cwd, targets: opts.target });
1435
+ skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex/qoder),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|qoder|all", "all").action(async (opts) => {
1436
+ const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
559
1437
  for (const s of summaries) {
560
1438
  if (s.codex) {
561
1439
  console.log(`[codex] AGENTS.md: ${s.codex.action}`);
@@ -563,6 +1441,9 @@ function buildProgram(cwd, version = "0.0.0") {
563
1441
  }
564
1442
  for (const e of s.entries) console.log(`[${s.target}] ${e.name}: ${e.action}`);
565
1443
  }
1444
+ console.log(`[types] ${".easytwin/types"}: ${types.action}`);
1445
+ if (types.tsconfig === "created") console.log("[types] \u5DF2\u751F\u6210\u6700\u5C0F tsconfig.json");
1446
+ if (types.pathsHint) console.log(types.pathsHint);
566
1447
  });
567
1448
  return program;
568
1449
  }