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