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