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